/*
 * Package il.ac.biu.cs.grossmm.impl.simple
 * File SipSubscriptionAdapter.java
 * 
 * This is a part of presence service framework prototype. 
 * See javadoc for more information.
 *  
 * Copyright (C) 2006 Grossmann Mark
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package il.ac.biu.cs.grossmm.impl.simple;

import static il.ac.biu.cs.grossmm.api.simple.SubscriptionEvent.APPROVED;
import static il.ac.biu.cs.grossmm.api.simple.SubscriptionEvent.NORESOURCE;
import static il.ac.biu.cs.grossmm.api.simple.SubscriptionEvent.SUBSCRIBE;
import static il.ac.biu.cs.grossmm.api.simple.SubscriptionEvent.TIMEOUT;
import static il.ac.biu.cs.grossmm.api.simple.SubscriptionState.ACTIVE;
import static il.ac.biu.cs.grossmm.api.simple.SubscriptionState.INIT;
import static il.ac.biu.cs.grossmm.api.simple.SubscriptionState.PENDING;
import static il.ac.biu.cs.grossmm.api.simple.SubscriptionState.TERMINATED;
import static il.ac.biu.cs.grossmm.api.simple.SubscriptionState.WAITING;
import gov.nist.javax.sip.message.SIPResponse;
import il.ac.biu.cs.grossmm.api.OperationFailedException;
import il.ac.biu.cs.grossmm.api.flow.Status;
import il.ac.biu.cs.grossmm.api.presence.Entity;
import il.ac.biu.cs.grossmm.api.presence.EntityType;
import il.ac.biu.cs.grossmm.api.presence.Registrar;
import il.ac.biu.cs.grossmm.api.presence.ResourceType;
import il.ac.biu.cs.grossmm.api.server.Component;
import il.ac.biu.cs.grossmm.api.server.ComponentManager;
import il.ac.biu.cs.grossmm.api.server.ServiceManager;
import il.ac.biu.cs.grossmm.api.simple.InternalSubscription;
import il.ac.biu.cs.grossmm.api.simple.Payload;
import il.ac.biu.cs.grossmm.api.simple.RequestProcessException;
import il.ac.biu.cs.grossmm.api.simple.SipEventPackage;
import il.ac.biu.cs.grossmm.api.simple.SipNotificationProvider;
import il.ac.biu.cs.grossmm.api.simple.SipRequestProcessor;
import il.ac.biu.cs.grossmm.api.simple.SipSharedProvider;
import il.ac.biu.cs.grossmm.api.simple.SipSubscription;
import il.ac.biu.cs.grossmm.api.simple.SubscriptionState;
import il.ac.biu.cs.grossmm.api.server.Service;

import java.text.ParseException;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.activation.MimeType;
import javax.activation.MimeTypeParameterList;
import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.RequestEvent;
import javax.sip.ResponseEvent;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.SipFactory;
import javax.sip.SipListener;
import javax.sip.SipStack;
import javax.sip.TimeoutEvent;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.AcceptHeader;
import javax.sip.header.ContentTypeHeader;
import javax.sip.header.EventHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.HeaderFactory;
import javax.sip.header.SubscriptionStateHeader;
import javax.sip.header.ToHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;

import org.apache.log4j.Logger;

/**
 * Implements SIP-specific subscription and event notification mechanism as
 * described in RFC 3265.
 * 
 */
public class SipSubscriptionAdapter implements Component, Service,
		SipRequestProcessor, SipNotificationProvider {

	static Logger logger = Logger.getLogger(SipSubscriptionAdapter.class);

	static final int NO_EXPIRES = -1;

	ComponentManager cMan;

	ServiceManager sMan;

	SipFactory sipFactory = null;

	SipStack sipStack = null;

	private SipSharedProvider provider;

	private HeaderFactory headerFactory;

	private Map<String, SipEventPackage> packages = new ConcurrentHashMap<String, SipEventPackage>();

	private Map<String, SipEventPackage> templates = new ConcurrentHashMap<String, SipEventPackage>();

	private Map<ClientTransaction, SipSubscription> transactions = new ConcurrentHashMap<ClientTransaction, SipSubscription>();

	protected Registrar registrar;

	@Override
	public String toString() {
		return "SIP Specific Event Notification Adapter (RFC 3265)";
	}

	public void setComponentManager(ComponentManager cMan) throws Exception {
		this.cMan = cMan;

		// Obtain SIP-specific object factories
		SipFactory sipFactory = SipFactory.getInstance();
		headerFactory = sipFactory.createHeaderFactory();

		provider = cMan.getByClass(SipSharedProvider.class);
	}

	public void start(ServiceManager sMan) throws Exception {
		this.sMan = sMan;

		// attach to the shared SIP provider to be able to receive SIP
		// requests/responses
		provider.addSipListener(this);
	}

	public void stop(Exception exception) {
		// detach from the shared SIP provider to stop receivng SIP
		// requests/responses

		provider.removeSipListener(this);
	}

	protected void doStart() {
		// attach to the shared SIP provider to be able to receive SIP
		// requests/responses

		provider.addSipListener(this);
	}

	protected void doStop() {
		// detach from the shared SIP provider to stop receivng SIP
		// requests/responses

		provider.removeSipListener(this);
	}

	// not synchronized
	public void addPackage(SipEventPackage eventPackage) {
		if (eventPackage.isTemplatePackage())
			templates.put(eventPackage.getPackageName(), eventPackage);
		else
			packages.put(eventPackage.getPackageName(), eventPackage);
	}

	// not synchronized
	public void removePackage(SipEventPackage eventPackage) {
		if (eventPackage.isTemplatePackage())
			templates.remove(eventPackage);
		else
			packages.remove(eventPackage);
	}

	/**
	 * Given SUBSCRIPTION request returns the value of expires header. If
	 * expires header is not included, the default expiration value is returned
	 * 
	 * @param request
	 * @return expiration value
	 */
	static int getExpires(Request request, SipEventPackage sipPackage) {
		ExpiresHeader expires = (ExpiresHeader) request
				.getHeader(ExpiresHeader.NAME);
		if (expires == null)
			return sipPackage.getDefaultExpiration();
		else
			return expires.getExpires();
	}

	/**
	 * @see SipListener
	 */
	public Response processRequest(RequestEvent requestEvent) {
		// TODO:
		// 3.1.6.3. "Authentication/Authorization of SUBSCRIBE requests" from
		// RFC 3265

		// TODO:
		// If the duration specified in a SUBSCRIBE message is unacceptably
		// short, the notifier may be able to send a 423 response, as
		// described earlier in this section
		// (not implemented here)

		if (logger.isDebugEnabled()) {
			logger.debug("processRequest() - start");
		}

		Request request = requestEvent.getRequest();
		ServerTransaction transaction = requestEvent.getServerTransaction();

		// Step 1. Method Header should be SUBSCRIBE
		String method = request.getMethod();
		if (!method.equals(Request.SUBSCRIBE)) {
			logger.debug("processRequest()  - exiting with null (method is "
					+ method + ")");
			return null; // the method name is not a "SUBSCRIBE", we do not
			// process it
		}

		/*
		 * Identification of events is provided by three pieces of information:
		 * Request URI, Event Type, and (optionally) message body.
		 */

		// Step 2. Check Event Header
		EventHeader eventHeader = (EventHeader) request
				.getHeader(EventHeader.NAME);

		// Step 2.1: Event Header should not be null
		//
		// Subscribers MUST include exactly one "Event" header in SUBSCRIBE
		// requests (RFC 3265).
		// Check that the header is there.
		if (eventHeader == null) {

			// If a server does not
			// support PINT, it SHOULD return "489 Bad Event" to any SUBSCRIBE
			// messages without an "Event" header.
			// [we are not going to support PINT]

			logger
					.debug("processRequest() - exiting with BAD_REQUEST (eventHeader == null)");

			return provider.createResponse(Response.BAD_REQUEST,
					"No event header (see RFC3265, section 3.1.2)");
		}

		// Step 2.2
		//
		// Check that the event package specified in the "Event" header is
		// understood (i.e. supported by some attached event package)
		//

		// obtain event type
		String eventType = eventHeader.getEventType();

		// get matching event package if any
		SipEventPackage eventPackage = getEventPackage(eventType);

		if (eventPackage == null) {
			// No package supports this event type, respond with 486

			if (logger.isDebugEnabled()) {
				logger
						.debug("processRequest() - exiting with BAD_EVENT (event-type is "
								+ eventType + ")");
			}

			return provider.createResponse(Response.BAD_EVENT,
					"Event type not supported: " + eventType);
		}

		// Step 3. Check MIME types specified in Accept header
		// 
		// Because SUBSCRIBE requests create a dialog as defined in SIP,
		// they MAY contain an "Accept" header. This header, if present,
		// indicates the body formats allowed in subsequent NOTIFY requests.
		//
		if (!compatibleMimeTypes(request, eventPackage)) {
			if (logger.isDebugEnabled()) {
				logger
						.debug("processRequest() - exiting with NOT_ACCEPTABLE (incompatible mime types)");
			}

			return provider.createResponse(Response.NOT_ACCEPTABLE,
					"None of specified MIME types is supported");
		}

		/*
		 * Step 4. Get the timeout (Expires header value if exists, or default
		 * package value otherwise)
		 */
		int timeout = getExpires(request, eventPackage);

		/*
		 * Step 5. Check id parameter of the Event header
		 * 
		 * The "Event" header MAY also contain an "id" parameter. This "id"
		 * parameter, if present, contains an opaque token which identifies the
		 * specific subscription within a dialog
		 */
		String eventId = eventHeader.getEventId();

		// The SUBSCRIBE request is valid at this point as long as there's no
		// event package specific errors and the watcher exists

		Response response = null; // the response as it will be built by an
		// event-package

		// are we within a dialog?
		if (transaction == null) {
			// not within an existing dialog - this is an initial SUBSCRIBE
			// request
			Entity watcher = getWatcher(request);

			if (logger.isDebugEnabled()) {
				logger
						.debug("processRequest() - exiting (after new subscription creation)");
			}

			// create the subscription
			response = createSubscription(request, eventPackage, watcher,
					eventId, timeout);
		} else {

			// we are within a dialog (may be an initial request as well as a
			// refresh)

			Dialog dialog = transaction.getDialog();

			assert dialog != null;

			Map map = getSubscriptionMap(dialog);

			String key = eventId == null ? "" : eventId;

			// get a subscription with this id, if any
			SipSubscription subscription = (SipSubscription) map.get(key);

			if (subscription == null) { // no such subscription
				// no matching subscriptions - this is an initial SUBSCRIBE
				// request

				Entity watcher = getWatcher(request);

				if (watcher == null)
					response = provider.createResponse(
							SIPResponse.UNAUTHORIZED, "Unauthorized watcher");

				else {
					createSubscription(request, eventPackage, watcher, eventId,
							timeout);
				}
			} else {
				// within an existing dialog - this is a subscription refresh
				response = processSubscriptionRefresh(request, transaction,
						subscription, timeout);
			}
		}

		if (response == null)
			return PROCESSED;
		else
			return toTagFix(response);
	}

	/**
	 * Ensure that To header tag is set
	 * 
	 * @param response
	 *            the response which should have a To tag to be set to some
	 *            value
	 * @return
	 */
	private Response toTagFix(Response response) {
		try {
			ToHeader to = (ToHeader) response.getHeader(ToHeader.NAME);
			if (logger.isDebugEnabled()) {
				logger.debug("Value of to: " + to);
			}
			if (to.getTag() == null) {
				to.setTag(Identifiers.generate16bitId());
			}
			if (logger.isDebugEnabled()) {
				logger.debug("Value of to tag: "
						+ ((ToHeader) response.getHeader(ToHeader.NAME))
								.getTag());
			}
		} catch (ParseException e) {
			// should never happen
			logger.fatal(e.getMessage(), e);
			return provider.createResponse(Response.SERVER_INTERNAL_ERROR,
					"Could not set To tag");
		}

		return response;
	}

	/**
	 * Creates an <code>ISipSubscription</code> instance based on initial
	 * subscription (a SUBSCRIBE request which does not match any existing
	 * subscription) and registers it by calling <code>addSubscription</code>
	 * method. Note that more than one <code>ISipSubscription</code> instances
	 * may be created/registered per call.
	 * 
	 * <p>
	 * This method or ISipSubscription object should send a SIP response to the
	 * SUBSCRIBE request. Note that "In no case should a SUBSCRIBE transaction
	 * extend for any longer than the time necessary for automated processing"
	 * (RFC 3265).
	 * </p>
	 * 
	 * <p>
	 * The method should not take too much time to complete in order not to
	 * block processing of other SIP messages.
	 * </p>
	 * 
	 * @param request
	 *            the SUBSCRIBE request which resulted in this method call
	 * @param pck
	 *            Sip event package for this subscription request
	 * @param watcher
	 *            the watcher who performs this subscription
	 * @param id
	 *            the subscription id (id parameter of the Event header)
	 * @param timeout
	 *            subscription timeout (value of the expires header or the
	 *            default value)
	 * @return SIP response to the SUBSCRIBE request
	 * 
	 */
	protected Response createSubscription(Request request, SipEventPackage pck,
			Entity watcher, String id, int timeout) {

		// the request is either not in dialog or is in dialog but does not
		// match any existing subscription and therefore is an initial subscribe
		try {
			ToHeader toHeader = (ToHeader) request.getHeader(ToHeader.NAME);

			if (toHeader == null)
				throw new RequestProcessException(Response.BAD_REQUEST,
						"No To header");

			URI uri = request.getRequestURI();

			// workaround for incorrectly constructed request (e.g. NIST IM
			// client)
			if (uri.isSipURI() && ((SipURI) uri).getUser() == null) {
				uri = toHeader.getAddress().getURI();
			}

			SipSubscriptionImpl sipSub = new SipSubscriptionImpl(pck, id);

			return pck.subscribe(uri, request, sipSub);

		} catch (RequestProcessException e) {

			Response resp = provider.createResponse(e.getCode(), e.getReason());

			try {
				provider.sendResponse(resp);
			} catch (SipException ignored) {
			}
			return resp;
		}
	}

	public void initialized(SipSubscription sipSub) {
		boolean success = initialized(sipSub.getInternalSubscription()
				.getStatus(), sipSub);

		if( ! success )
			close(sipSub);
	}

	private boolean initialized(Status status, SipSubscription sipSub) {
		Response resp;
		// initializing -> no dialog created yet
		assert sipSub.getDialog() == null;

		boolean success;

		if (status.isKindOf(Status.NORESOURCE)
				|| status.isKindOf(Status.UNMATCHED)) {
			resp = provider.createResponse(Response.NOT_FOUND, status
					.getReason());
			try {
				provider.sendResponse(resp);
			} catch (SipException e) {
				// ignore.
				logger.warn(e);
			}
			success = false;
		} else {
			// create subscription dialog

			ServerTransaction transaction = provider.startServerTransaction();
			sipSub.setDialog(transaction.getDialog());
			addSubscription(sipSub);

			if (status == Status.ACTIVE)
				resp = provider.createResponse(Response.OK, "Subscribed");
			else
				resp = provider.createResponse(Response.ACCEPTED, status
						.getReason());

			try {
				provider.sendResponse(resp);
			} catch (SipException e) {
				// ignore.
				logger.warn(e);
			}
			
			success = setState(status, sipSub);
		}
				
		return success;
	}

	private boolean setState(Status status, SipSubscription sipSub) {
		assert sipSub.getDialog() != null;

		if (status == Status.ACTIVE) {
			sipSub.setState(APPROVED, ACTIVE, "Approved");
		} else if (status == Status.PENDING) {
			sipSub.setState(SUBSCRIBE, PENDING, status.getReason());
		} else if (status == Status.NORESOURCE) {
			sipSub.setState(NORESOURCE, TERMINATED, status.getReason());
		} else if (status == Status.UNMATCHED) {
			sipSub.setState(NORESOURCE, TERMINATED, "Unmatched");
		}

		processUpdate(sipSub);

		SubscriptionState state = sipSub.getState();
		return state != TERMINATED && state != WAITING;
	}

	public void setState(InternalSubscription internal, Status status) {
		Set<SipSubscription> sipSubs = internal.getSipSubscriptrions();

		for (Iterator<SipSubscription> it = sipSubs.iterator(); it.hasNext();) {
			SipSubscription sipSub = it.next();
			if (sipSub.getState() == INIT) {
				// non-initialized subscription
				boolean success = initialized(status, sipSub);

				if (!success) {
					it.remove();
					continue;
				}
			} else {

				// initialized subscription

				if (!setState(status, sipSub))
					it.remove();
			}
		}
	}

	/**
	 * Checks whether the MIME type specified in the Accept header of the
	 * SUBSCRIBE request are compatible with at least one MIME type of the
	 * matching event package
	 * 
	 * @param request
	 *            the SUBSCRIBE request to check
	 * @param eventPackage
	 *            the matching event package
	 * 
	 * @return false if incompability detected, true otherwise (compatible or
	 *         cannot be determined)
	 */
	private boolean compatibleMimeTypes(Request request,
			SipEventPackage eventPackage) {
		ListIterator lit = request.getHeaders(AcceptHeader.NAME);
		if (!lit.hasNext())
			return true; // no accept header - cannot determine mime
		// compatibility

		while (lit.hasNext()) {
			AcceptHeader acceptHeader = (AcceptHeader) lit.next();

			for (String[] mime : eventPackage.getMimeTypes()) {
				boolean typeMatches = acceptHeader.allowsAllContentTypes()
						|| acceptHeader.getContentType().equals(mime[0]);

				boolean subtypeMatches = acceptHeader
						.allowsAllContentSubTypes()
						|| acceptHeader.getContentSubType().equals(mime[1]);

				if (typeMatches && subtypeMatches)
					return true;
			}
		}

		return false;
	}

	/**
	 * @param eventType
	 * @return
	 */
	private SipEventPackage getEventPackage(String eventType) {
		SipEventPackage eventPackage = null;

		int p = eventType.lastIndexOf('.');
		if (p == -1) {
			eventPackage = packages.get(eventType);
		} else {
			eventPackage = templates.get(eventType.substring(p + 1));
		}
		return eventPackage;
	}

	/**
	 * @param dialog
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private ConcurrentHashMap<String, SipSubscription> getSubscriptionMap(
			Dialog dialog) {
		ConcurrentHashMap map = (ConcurrentHashMap) dialog.getApplicationData();

		if (map == null)
			dialog.setApplicationData(map = new ConcurrentHashMap());

		return map;
	}

	Entity getWatcher(Request request) {
		FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);
		URI uri = fromHeader.getAddress().getURI();

		// TODO: non-sip uri's

		SipURI sipUri = (SipURI) uri;

		String user = sipUri.getUser();

		Entity watcher;
		try {
			watcher = registrar.getEntity(user);
		} catch (OperationFailedException e) {
			e.printStackTrace();
			return null;
		}

		if (watcher.is(EntityType.WATCHER))
			return watcher;
		else
			return null;
	}

	/**
	 * Process request for existing subscription refresh.
	 * 
	 * @param request
	 *            the SUBSCRIBE request
	 * @param transaction
	 *            the matching transaction
	 * @param subscription
	 *            the matching subscription
	 * 
	 * @return response to send back, null if response has already been sent
	 */
	private Response processSubscriptionRefresh(Request request,
			ServerTransaction transaction, SipSubscription subscription,
			int timeout) {

		if (logger.isDebugEnabled()) {
			logger.debug("processSubscriptionRefresh() - start");
		}

		Response response;

		if (timeout == 0) {
			terminateSubscription(transaction, subscription);
			response = provider.createResponse(Response.OK, null);
		} else {
			SipEventPackage eventPackage = subscription.getEventPackage();
			response = eventPackage.refresh(request, subscription);
		}

		if (logger.isDebugEnabled()) {
			logger.debug("processSubscriptionRefresh() - end");
		}

		return response;
	}

	private void terminateSubscription(ServerTransaction transaction,
			SipSubscription sipSub) {

		close(sipSub);

		// NOTE: the reason phrase seem to be unspecified in the RFC ...

		// terminate the dialog
		transaction.getDialog().delete();
	}

	private void close(SipSubscription sipSub) {
		sipSub.getInternalSubscription().removeSipSubscripttion(sipSub);
		Dialog dialog = sipSub.getDialog();
		if( dialog != null )
			dialog.delete();
	}

	public boolean processResponse(ResponseEvent responseEvent) {
		ClientTransaction transaction = responseEvent.getClientTransaction();

		// check if there's a subscripton associated with this transaction
		SipSubscription subscription = transactions.get(transaction);

		if (subscription == null)
			// do not process this response here
			return false;

		Response response = responseEvent.getResponse();
		int code = response.getStatusCode();

		/*
		 * A NOTIFY request is considered failed if the response times out, or a
		 * non-200 class response code is received which has no "Retry-After"
		 * header and no implied further action which can be taken to retry the
		 * request (e.g., "401 Authorization Required".)
		 * 
		 * ...
		 * 
		 * If the NOTIFY request fails (as defined above) due to an error
		 * response, and the subscription was installed using a soft-state
		 * mechanism, the notifier MUST remove the corresponding subscription.
		 * 
		 * (RFC 3265, 3.2.2)
		 */

		if (code / 100 != 2) {
			// not 2xx code - deactivate the subscription

			logger.warn("Non-2xx responce received: " + response);

			close(subscription);
			return true;
		}

		// TODO: handle non-fatal codes such as 401 and responses with
		// Retry-After

		return true;
	}

	public boolean processTimeout(TimeoutEvent timeoutEvent) {
		ClientTransaction transaction = timeoutEvent.getClientTransaction();

		// check if there's a subscripton associated with this transaction
		SipSubscription sipSub = transactions.get(transaction);

		if (sipSub == null)
			// do not process this response here
			return false;

		/*
		 * If the NOTIFY request fails (as defined above) due to a timeout
		 * condition, and the subscription was installed using a soft-state
		 * mechanism (such as SUBSCRIBE), the notifier SHOULD remove the
		 * subscription.
		 * 
		 * (RFC 3265, 3.2.2)
		 */

		sipSub.setState(TIMEOUT, TERMINATED, "Timeout");

		processUpdate(sipSub);

		return true;
	}

	private boolean addSubscription(SipSubscription subscription) {
		String id = subscription.getId();
		String key = id == null ? "" : id;

		ConcurrentHashMap<String, SipSubscription> map = getSubscriptionMap(subscription
				.getDialog());

		return map.putIfAbsent(key, subscription) == null;
	}

	private void removeSubscription(SipSubscription subscription) {
		String id = subscription.getId();
		String key = id == null ? "" : id;

		ConcurrentHashMap<String, SipSubscription> map = getSubscriptionMap(subscription
				.getDialog());

		map.remove(key);

		InternalSubscription internal = subscription.getInternalSubscription();
		if (internal != null)
			internal.removeSipSubscripttion(subscription);
	}

	@SuppressWarnings("unchecked")
	public Request buildNotifyRequest(SipSubscription sipSub, Payload payload) {
		try {
			Dialog dialog = sipSub.getDialog();

			// Create the Notify request and add som headers
			Request notify = dialog.createRequest(Request.NOTIFY);

			// Add Event Header
			notify.addHeader(headerFactory.createEventHeader(sipSub
					.getEventPackage().getPackageName()));

			// Create SubscriptionState Header (add later)
			SubscriptionStateHeader ssH = headerFactory
					.createSubscriptionStateHeader(sipSub.getState().toString()
							.toLowerCase());

			String reasonCode = sipSub.getEvent().toString().toLowerCase();

			ssH.setReasonCode(reasonCode);

			if (sipSub.getState() == ACTIVE) {
				int expires = (int) ((sipSub.getTimeoutTime() - System
						.currentTimeMillis()) / 1000);

				// Add expires parameter to Subscription state header
				if (expires > 0)
					ssH.setExpires(expires);
				else
					ssH.setExpires(1); // not final
			} else {
				ssH.setExpires(0); // final
			}

			// Add SubscriptionStateHeader
			notify.addHeader(ssH);

			byte[] bytes = payload.getBytes();

			if (bytes != null) {
				MimeType mime = payload.getMimeType();

				ContentTypeHeader cth = headerFactory.createContentTypeHeader(
						mime.getPrimaryType(), mime.getSubType());

				MimeTypeParameterList parameters = mime.getParameters();

				Enumeration<String> names = parameters.getNames();

				while (names.hasMoreElements()) {
					String name = names.nextElement();
					String value = parameters.get(name);
					cth.setParameter(name, value);
				}

				notify.setContent(bytes, cth);
			}

			// Add Authorization Header?

			logger.debug("Notification request constructed: \n"
					+ notify.toString());
			return notify;
		} catch (Exception e) {
			logger.fatal("Cannot build notification", e);
			return null;
		}
	}

	public void sendNotifyRequest(SipSubscription sipSubscription,
			Request request) {
		Dialog dialog = sipSubscription.getDialog();

		try {
			ClientTransaction clientTransaction = provider
					.getNewClientTransaction(request);

			// Send Notify
			synchronized (this) {
				dialog.sendRequest(clientTransaction);
				transactions.put(clientTransaction, sipSubscription);
			}
		} catch (Exception e) {
			logger.fatal("Cannot send notification", e);
		}
	}

	private void processUpdate(SipSubscription sipSub) {
		Request notifyRequest;
		SubscriptionState state = sipSub.getState();
		if (state == ACTIVE) {
			Payload payload = sipSub.getInternalSubscription().getPayload(
					sipSub);
			notifyRequest = buildNotifyRequest(sipSub, payload);
		} else
			notifyRequest = buildNotifyRequest(sipSub, null);

		sendNotifyRequest(sipSub, notifyRequest);

		if (state == TERMINATED || state == WAITING) {
			removeSubscription(sipSub);
		}
	}

	public SipSharedProvider getSharedProvider() {
		return provider;
	}

	public ResourceType getResourceType(String eventType) {
		// get matching event package if any
		SipEventPackage eventPackage = getEventPackage(eventType);

		if (eventPackage == null)
			return null;
		else
			return eventPackage.getResourceType(eventType);
	}

	public void aborted(Object hint, Exception e) throws Exception {
		throw e;
	}

	public void terminated(Object serviceHint, Exception e) throws Exception {
		stop(e);
		throw e;
	}

}
